函数

概述

JavaScript 有三种声明函数的方法。

function 命令

function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。

函数表达式

除了用function命令声明函数,还可以采用变量赋值的写法。

这种写法将一个匿名函数赋值给变量。

采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效

这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。

注意,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。

Function 构造函数

你可以传递任意数量的参数给Function构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。

这种声明函数的方式非常不直观,几乎无人使用

函数的重复声明

如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。

圆括号运算符,return 语句和递归

第一等公民

JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。

由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。

函数名的提升

JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。

由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错

不能在条件语句中声明函数

根据 ES5 的规范,不得在非函数的代码块中声明函数,最常见的情况就是if和try语句。

但是,实际情况是各家浏览器往往并不报错,能够运行。

但是由于存在函数名的提升,所以在条件语句中声明函数,可能是无效的,这是非常容易出错的地方。

要达到在条件语句中定义函数的目的,只有使用函数表达式。

函数的属性和方法

name属性

函数的name属性返回函数的名字。

如果是通过变量赋值定义的函数,那么name属性返回变量名。

只有在变量的值是一个匿名函数时才是如此。如果变量的值是一个具名函数,那么name属性返回function关键字之后的那个函数名。

name属性的一个用处,就是获取参数函数的名字。

length属性

函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。

length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的”方法重载“(overload)。

toString()

函数的toString方法返回一个字符串,内容是函数的源码。

函数内部的注释也可以返回。

函数作用域

定义

函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。

在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。

函数内部定义的变量,会在该作用域内覆盖同名全局变量。

函数内部的变量提升

与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

函数本身的作用域

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

参数

概述

函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。

参数的省略

函数参数不是必需的,Javascript 允许省略参数。

运行时无论提供多少个参数(或者不提供参数),JavaScript 都不会报错。省略的参数的值就变为undefined。

注意,函数的length属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。

传递方式

函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。

如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。

注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。

重新对o赋值导致o指向另一个地址,保存在原地址上的值当然不受影响。

同名参数

如果有同名的参数,则取最后出现的那个值。

arguments 对象

由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。

rguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。

正常模式下,arguments对象可以在运行时修改。

严格模式下,arguments对象是一个只读对象,修改它是无效的,但不会报错。

通过arguments对象的length属性,可以判断函数调用时到底带几个参数。

与数组的关系

需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。

callee属性

arguments对象带有一个callee属性,返回它所对应的原函数。

可以通过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。

函数的其他知识点

闭包

闭包(closure)是 Javascript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。

原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

闭包的另一个用处,是封装对象的私有属性和私有方法。

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

立即调用的函数表达式(IIFE)

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

eval 命令

eval命令的作用是,将字符串当作语句执行。

eval的命令字符串不会得到 JavaScript 引擎的优化,运行速度较慢。这也是一个不应该使用它的理由。

运算符

加法运算符

JavaScript 允许非数值的相加。如果是两个字符串相加,这时加法运算符会变成连接运算符,返回一个新的字符串,将两个原字符串连接在一起。如果一个运算子是字符串,另一个运算子是非字符串,这时非字符串会转成字符串,再连接在一起。

除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。

对象的相加

如果运算子是对象,必须先转成原始类型的值,然后再相加。

算数运算符

余数运算符运算结果的正负号由第一个运算子的正负号决定。

余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。

赋值运算符

比较运算符

相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小。

字符串的比较

字符串按照字典顺序进行比较。

JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。

非字符串的比较

两个原始类型的值的比较,除了相等运算符(==)和严格相等运算符(===),其他比较运算符都是先转成数值再比较。

字符串和布尔值都会先转成数值,再进行比较。

特殊情况,即任何值(包括NaN本身)与NaN比较,返回的都是false。

对象

严格相等运算符

JavaScript 提供两种相等运算符:==和===。

简单说,它们的区别是相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将它们转换成同一个类型,再用严格相等运算符进行比较。

需要注意的是,NaN与任何值都不相等(包括自身)。另外,正0等于负0。

两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址。使用===

空对象、空数组、空函数的值,都存放在不同的内存地址。

注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值。

undefined和null
undefined和null与自身严格相等。

由于变量声明后默认值是undefined,因此两个只声明未赋值的变量是相等的。

严格不相等运算符

严格相等运算符有一个对应的“严格不相等运算符”(!==),它的算法就是先求严格相等运算符的结果,然后返回相反值。

相等运算符

对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转化成原始类型的值,再进行比较。

undefined和null与其他类型的值比较时,结果都为false,它们互相比较时结果为true。

相等运算符隐藏的类型转换,会带来一些违反直觉的结果。

因此不要使用相等运算符(==),最好只使用严格相等运算符(===)。

不相等运算符

相等运算符有一个对应的“不相等运算符”(!=),两者的运算结果正好相反。

布尔运算符

对于非布尔值,取反运算符会将其转为布尔值。可以这样记忆,以下六个值取反后为true,其他值都为false。

如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean函数的作用相同。这是一种常用的类型转换的写法。

&&

||

(?:)

位运算符

位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。另外,虽然在 JavaScript 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数。

左移运算符(<<)表示将一个数的二进制值向左移动指定的位数,尾部补0,即乘以2的指定次方(最高位即符号位不参与移动)。

右移运算符(>>)表示将一个数的二进制值向右移动指定的位数,头部补0,即除以2的指定次方(最高位即符号位不参与移动)。

带符号位的右移运算符(>>>)表示将一个数的二进制形式向右移动,包括符号位也参与移动,头部补0。所以,该运算总是得到正值。对于正数,该运算的结果与右移运算符(>>)完全一致,区别主要在于负数。

开关作用

掩码

其他运算符

void运算符

void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined。

这个运算符的主要用途是浏览器的书签工具(bookmarklet),以及在超级链接中插入代码防止网页跳转。

逗号运算符

逗号运算符用于对两个表达式求值,并返回后一个表达式的值。

运算顺序

优先级

五个运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)。

记住所有运算符的优先级,是非常难的,也是没有必要的。

数据类型转换

JavaScript 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。

虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算子的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。

强制转换

强制转换主要指使用Number、String和Boolean三个函数,手动将各种类型的值,分布转换成数字、字符串或者布尔值。

Number()

使用Number函数,可以将任意类型的值转化成数值。

Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。

parseInt和Number函数都会自动过滤一个字符串前导和后缀的空格。

Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组

第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。
第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。
第三步,如果toString方法返回的是对象,就报错。

String()

String函数可以将任意类型的值转化成字符串,转换规则如下。

String方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。

String方法背后的转换规则,与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序。

Boolean()

Boolean函数可以将任意类型的值转为布尔值。

它的转换规则相对简单:除了以下五个值的转换结果为false,其他的值全部为true。

所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true。

自动转换

自动转换是以强制转换为基础的。

以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见。

第一种情况,不同类型的数据互相运算。

第二种情况,对非布尔值类型的数据求布尔值。

第三种情况,对非数值类型的值使用一元运算符(即+和-)。

自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String函数进行转换。

由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用Boolean、Number和String函数进行显式转换。

自动转换为布尔值

自动转换为字符串

字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。

这种自动转换很容易出错。期望求导个整数值结果得到个字符串。

自动转换为数值

除了加法运算符(+)有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。

注意:null转为数值时为0,而undefined转为数值时为NaN。

一元运算符也会把运算子转成数值。

错误处理机制

Error 实例对象

JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例。

原生错误类型

Error实例对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他6种错误对象。也就是说,存在Error的6个派生对象。

SyntaxError 对象

SyntaxError对象是解析代码时发生的语法错误。

ReferenceError 对象

ReferenceError对象是引用一个不存在的变量时发生的错误。

另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值。

RangeError 对象

RangeError对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。

TypeError 对象

TypeError对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数。

URIError 对象

URIError对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()这六个函数。

EvalError 对象

eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留。

自定义错误

throw语句

throw语句的作用是手动中断程序执行,抛出一个错误。

实际上,throw可以抛出任何类型的值。也就是说,它的参数可以是任何值。

对于 JavaScript 引擎来说,遇到throw语句,程序就中止了。引擎会接收到throw抛出的信息,可能是一个错误实例,也可能是其他类型的值。

try…catch 结构

一旦发生错误,程序就中止执行了。JavaScript 提供了try…catch结构,允许对错误进行处理,选择是否往下执行。

如果你不确定某些代码是否会报错,就可以把它们放在try…catch代码块之中,便于进一步对错误进行处理。

catch代码块之中,还可以再抛出错误,甚至使用嵌套的try…catch结构。

为了捕捉不同类型的错误,catch代码块之中可以加入判断语句。

finally 代码块

try…catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。

return语句的执行是排在finally代码之前,只是等finally代码执行完毕后才返回。

由于没有catch语句块,所以错误没有捕获。执行finally代码块以后,程序就中断在错误抛出的地方。

try…catch…finally这三者之间的执行顺序。

尽量不要在finally中使用return语句,如果使用的话,会忽略try、catch中的返回语句,也会忽略try、catch中的异常,屏蔽了错误的发生

finally中避免再次抛出异常,一旦finally中发生异常,代码执行将会抛出finally中的异常信息,try、catch中的异常将被忽略

所以在实际项目中,finally常常是用来关闭流或者数据库资源的,并不额外做其他操作。

编程风格

概述

编程风格的选择不应该基于个人爱好、熟悉程度、打字量等因素,而要考虑如何尽量使代码清晰易读、减少出错。你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格。这一点,对于 JavaScript 这种语法自由度很高的语言尤其重要。

缩进

行首的空格和 Tab 键,都可以产生代码缩进效果(indent)。

Tab 键可以节省击键次数,但不同的文本编辑器对 Tab 的显示不尽相同,有的显示四个空格,有的显示两个空格,所以有人觉得,空格键可以使得显示效果更统一。

区块

如果循环和判断的代码体只有一行,JavaScript 允许该区块(block)省略大括号。

建议总是使用大括号表示区块。

JavaScript 要使用起首的大括号跟在关键字的后面,因为 JavaScript 会自动添加句末的分号,导致一些难以察觉的错误。

因此,表示区块起首的大括号,不要另起一行。

圆括号

圆括号(parentheses)在 JavaScript 中有两种作用,一种表示函数的调用,另一种表示表达式的组合(grouping)。

建议可以用空格,区分这两种不同的括号。

行尾的分号

不使用分号的情况

for和while循环

注意,do…while循环是有分号的。

分支语句:if,switch,try

函数的声明语句

注意,函数表达式仍然要使用分号。

以上三种情况,如果使用了分号,并不会出错。因为,解释引擎会把这个分号解释为空语句。

分号的自动添加

所有语句都应该使用分号。但是,如果没有使用分号,大多数情况下,JavaScript 会自动添加。

麻烦的是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript 就不会自动添加分号。

如果continue、break、return和throw这四个语句后面,直接跟换行符,则会自动添加分号。这意味着,如果return语句返回的是一个对象的字面量,起首的大括号一定要写在同一行,否则得不到预期结果。

由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。

不应该省略结尾的分号,还有一个原因。有些 JavaScript 代码压缩器(uglifier)不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。

另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号。

上面这种写法就可以避免与其他脚本合并时,排在前面的脚本最后一行语句没有分号,导致运行出错的问题。

全局变量

JavaScript 最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用,非常不利。

因此,建议避免使用全局变量。如果不得不使用,可以考虑用大写字母表示变量名,这样更容易看出这是全局变量,比如UPPER_CASE。

变量声明

JavaScript 会自动将变量声明”提升“(hoist)到代码块(block)的头部。

为了避免可能出现的问题,最好把变量声明都放在代码块的头部。

所有函数都应该在使用之前定义。函数内部的变量声明,都应该放在函数的头部。

with语句

with可以减少代码的书写,但是会造成混淆。

因此,不要使用with语句。

相等和严格相等

相等运算符会自动转换变量类型,造成很多意想不到的情况。

建议不要使用相等运算符(==),只使用严格相等运算符(===)。

语句的合并

建议不要将不同目的的语句,合并成一行。

自增和自减运算符

自增(++)和自减(–)运算符,放在变量的前面或后面,返回的值不一样,很容易发生错误。事实上,所有的++运算符都可以用+= 1代替。

建议自增(++)和自减(–)运算符尽量使用+=和-=代替。

switch…case结构

switch…case结构要求,在每一个case的最后一行必须是break语句,否则会接着运行下一个case。这样不仅容易忘记,还会造成代码的冗长。

建议switch…case结构可以用对象结构代替。